上一篇介紹了 Projection Pushdown,讓資料來源盡量只讀取查詢需要的欄位。
今天介紹另一個重要的最佳化:
Filter Pushdown
它的核心概念是:
將
WHERE條件盡可能往資料來源方向移動,越早排除不符合條件的資料,後續需要處理的資料就越少。
有讀者提問:
到底哪些資料來源支援 Pushdown?哪些不支援?
這不能只用「支援」或「不支援」二分法回答,因為實際能力取決於:
TableProvider 或 FileSource 的實作可以先用下表理解常見情況:
| 資料來源 | Filter Pushdown 情況 | 常見行為 |
|---|---|---|
| Parquet | 通常支援部分條件 | 利用 Row Group Statistics 跳過不可能符合的資料區塊 |
| CSV | 通常較有限 | 多半先解析資料,再由 FilterExec 篩選 |
| JSON / NDJSON | 通常較有限 | 需要先解析 JSON,較難直接跳過大量資料 |
| MemoryTable | 通常由 DataFusion 處理 | 資料已在記憶體中,主要減少下游運算量 |
| 外部資料庫或 API | 取決於 TableProvider 實作 |
可將條件轉換成資料來源自己的查詢 |
因此,更精確的說法是:
DataFusion 會嘗試將 Filter 傳給資料來源,但資料來源是否能在讀取階段真正套用,取決於它支援的能力。
在 SQL 中,Filter 通常就是 WHERE 條件:
SELECT city, amount
FROM orders
WHERE amount > 1000;
這段查詢只保留:
amount > 1000
的資料列。
如果資料表有 100 萬筆資料,最直接的做法可能是:
讀取全部資料
↓
建立完整 RecordBatch
↓
套用 WHERE
↓
丟棄不符合條件的資料
這樣雖然結果正確,但可能已經花費大量成本讀取最後會被丟棄的資料。
假設只有 1% 的資料符合條件:
資料來源:1,000,000 rows
↓
讀取全部資料
↓
FilterExec
↓
剩下 10,000 rows
也就是:
讀取:1,000,000 rows
保留:10,000 rows
丟棄:990,000 rows
這會增加:
如果資料來源支援 Filter Pushdown,流程可能變成:
資料來源:1,000,000 rows
↓
資料來源層級先套用條件
↓
讀取 10,000 rows
↓
交給後續執行算子
這樣可以避免讀取大量不需要的資料。
需要區分兩個概念:
FilterExec
→ DataFusion 執行階段的篩選算子
Filter Pushdown
→ 將條件交給更底層的資料來源處理
如果 Physical Plan 是:
DataSourceExec
↓
FilterExec
代表資料可能先被讀出來,再由 FilterExec 篩選。
真正的資料來源層級 Pushdown 則是:
Filter: amount > 1000
↓
TableProvider / FileSource
↓
資料來源先篩選或跳過資料
即使最後仍看到 FilterExec,也不代表完全沒有 Pushdown。資料來源可能先做部分篩選,再由 FilterExec 完成最後確認。
Parquet 通常會將資料分成多個 Row Group,並儲存欄位統計資訊。
例如:
Row Group 1
amount:
min = 100
max = 500
Row Group 2
amount:
min = 800
max = 2,000
查詢條件是:
WHERE amount > 1000;
DataFusion 可以判斷:
Row Group 1 的最大值只有 500,
不可能有 amount > 1000 的資料。
因此可以跳過:
Row Group 1 → 跳過
Row Group 2 → 讀取
這種根據統計資訊跳過資料區塊的方式,通常稱為:
Predicate Pruning
SQL
WHERE amount > 1000
↓
Logical Plan 的 Filter
↓
Parquet Data Source
↓
讀取 Row Group Statistics
↓
跳過不可能符合條件的 Row Group
↓
讀取剩餘資料
注意,Row Group Pruning 只能跳過「確定不可能符合」的資料區塊。
如果統計資訊是:
amount:
min = 100
max = 5,000
查詢條件是:
WHERE amount > 1000;
這個 Row Group 仍可能包含符合條件的資料,因此不能整個跳過。
CSV 是文字列格式:
city,amount,is_member
Taipei,1200.0,true
Taichung,850.0,false
CSV 通常沒有 Parquet 的 Row Group Statistics。讀取器需要先:
WHERE 條件流程比較接近:
CSV
↓
解析資料列
↓
建立 RecordBatch
↓
FilterExec
↓
保留符合條件的資料
所以 CSV 即使能在計畫層知道有篩選條件,也不一定能在檔案讀取階段跳過大量資料。
如果資料已經放在記憶體中,例如 MemTable:
MemoryExec
↓
FilterExec
此時資料已經存在 Arrow RecordBatch 中,Pushdown 的重點不是減少磁碟 I/O,而是:
DataFusion 可以透過自訂 TableProvider 連接外部資料來源:
PostgreSQL
MySQL
REST API
遠端資料服務
如果 TableProvider 能理解查詢條件,就可以將:
WHERE amount > 1000
轉換成資料來源自己的查詢。
DataFusion Filter
↓
TableProvider
↓
外部資料庫先篩選
↓
只回傳符合條件的資料
在 Rust 的自訂 TableProvider 中,可以透過 supports_filters_pushdown 回報每個條件的支援程度:
Exact
→ 資料來源完整處理條件
Inexact
→ 資料來源可以減少資料,但仍可能需要 FilterExec
Unsupported
→ 資料來源不處理,由 DataFusion 篩選
這讓 DataFusion 知道哪些條件可以安全地下推。
實際查詢通常會同時使用兩種 Pushdown:
SELECT city, amount
FROM orders
WHERE is_member = true
AND amount > 1000;
這段查詢需要的欄位是:
city
amount
is_member
可能的計畫如下:
Projection: city, amount
Filter: is_member = true AND amount > 1000
TableScan: orders
projection=[city, amount, is_member]
兩者分工如下:
Projection Pushdown
→ 減少欄位數量
Filter Pushdown
→ 減少資料列數量
is_member 雖然不會出現在輸出結果中,但因為它被 WHERE 使用,所以仍然必須讀取。
在 DataFusion CLI 執行:
EXPLAIN FORMAT INDENT
SELECT city, amount
FROM orders
WHERE is_member = true
AND amount > 1000;
可能會看到:
logical_plan
Projection: orders.city, orders.amount
Filter: orders.is_member = Boolean(true)
AND orders.amount > Int64(1000)
TableScan: orders
Physical Plan 可能接近:
physical_plan
ProjectionExec
FilterExec
DataSourceExec
不同資料來源與 DataFusion 版本,輸出格式可能不同。
可以使用:
EXPLAIN ANALYZE
SELECT city, amount
FROM orders
WHERE is_member = true
AND amount > 1000;
觀察:
FilterExec 的輸入與輸出資料量如果資料來源仍讀取完整資料,再由 FilterExec 篩選,通常表示條件沒有在資料來源層級完全生效。
今天透過讀者的問題,釐清了 Filter Pushdown 的支援情況:
FilterExec 篩選TableProvider 實作 PushdownExact、Inexact 與 Unsupported 三種結果FilterExec 不代表一定沒有發生 PushdownEXPLAIN ANALYZE 可以協助觀察實際執行效果可以用一句話總結:
Filter Pushdown 讓資料在越靠近來源的地方越早被過濾,減少不必要的資料讀取與後續運算;但是否能真正降低 I/O,取決於資料來源的實作能力。
下一篇將介紹 Parquet Metadata、Statistics 與 Predicate Pruning,進一步了解資料來源如何根據統計資訊跳過不必要的 Row Group。